'/
' RabNet (Rabid Networking)
' 
' This is a strimmed down BlitzMax port of RakNet. It is basically inspired
' by RakNet, but is very strimmed down, and a bunch of new additional
' features has been added, all written from scratch.
' 
' Copyright (c) 2003 Jenkins Software LLC.
' RabNet written 2010 by Anton Berlin.
' 
' =========================================================================
' 
' All new features is free to use in any way, as long as you inlude
' Anton Berlin (or Anthony Berlin) as the orinial writer of the code.
' 
' All old features are subject to the appropriate license agreement.
' Creative Commons Licensees are subject to the license found at
' http://creativecommons.org/licenses/by-nc/2.5/
' 
' Single application licensees are subject to the license found at
' http://www.jenkinssoftware.com/SingleApplicationLicense.html
' 
' Custom license users are subject to the terms therein.
' 
' GPL license users are subject to the GNU General Public
' License as published by the Free Software Foundation; either version 3
' of the License, or (at your option) any later version.
'/

'// Struct representing a connected system.
type remoteSystem

	field m_isActive:byte = false; '// Is this instance active?
	field m_systemAddress:SystemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS;
	field m_weInitiatedTheConnection:byte = false; '// True if we started this connection via 'connect()'.
	                                               '// False if someone else connected to us.
	field m_lowestPing:int; '// The lowest ping value encountered.
	field m_lastPing:int;
	field m_clockDifferential:int;
	field m_nextPingTime:int; '// When to next ping this remote system.
	field m_connectionTime:int; '// Connection time, if active.
	field m_guid:long = RabNet.UNASSIGNED_RABNET_GUID; '// Globally unique identity of this remote system.
	field m_rabNetSocket:RabNetSocket; '// The socket representing this remote system.
	field m_connectionMode:short = NO_ACTION; '// What mode the connection is currently in.
	field m_listLink:TLink;
	
	'// Connection states.
	global NO_ACTION:short = 0;
	global CONNECTED:short = 1;
	global REQUESTED_CONNECTION:short = 2;
	global HANDLING_CONNECTION_REQUEST:short = 3;
	global UNVERIFIED_SENDER:short = 4;
	global DISCONNECT_ASAP:short = 5;
	global DISCONNECT_ASAP_SILENTLY:short = 6;
	global DISCONNECT_ON_NO_ACK:short = 7;

end type

private

'// Struct representing connection requests.
type connectionRequest

	field m_ip:int; '// Remote IP address of the system we're trying to connect to.
	field m_port:int; '// Port to the system we're trying to connect to.
	field m_timer:int; '// How long before we try to connect again.
	field m_password:string; '// Password data.

end type

'// Mutexes for RabPeer.
const incomingPacketPool_Mutex:short = 0;
const outgoingPacketPool_Mutex:short = 1;
const requestConnectionList_Mutex:short = 2;
const NUMBER_OF_RABPEER_MUTEXES:short = 3;

public

'// Interface class for a network peer.
type RabPeer extends Base

	'// -----------------------------------------------------------------------------------------------------------------
	'// Create a RabPeer and generate a GUID for it. It also creates all mutexes.
	'// -----------------------------------------------------------------------------------------------------------------
	function create:RabPeer()
	
		local p:RabPeer = new RabPeer;
		p.generateGUID();
		p.createMutexes();
		return p;
	
	end function
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Destructor. Automagically shutdowns the peer.
	'// -----------------------------------------------------------------------------------------------------------------
	method delete()
	
		shutdown(0);
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Generate a GUID.
	'// -----------------------------------------------------------------------------------------------------------------
	method generateGUID()
	
		local newGuid:string;
		
		delay(rand(1, 3)); '// To be sure that if we start two peers at the same time, they won't get the same GUID.
		local guid:string = milliSecs();
		
		local _hostname:string = hostname(hostip("localhost", 0));
		for local index:int = 0 until _hostname.length
		
			if (index > 9) ..
				exit;
			
			guid :+ _hostname[index] shl index shr rand(9, 10);
		
		next
		
		local segs:int = guid.length / 8;
		for local index:int = 0 until segs
		
			newGuid :+ int(guid[index*8..(index+1)*8]) ~ rand(1, 7);
		
		next
		
		m_guid = newGuid.toLong();
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Create all mutexes.
	'// -----------------------------------------------------------------------------------------------------------------
	method createMutexes()
	
		for local i:int = 0 until NUMBER_OF_RABPEER_MUTEXES
		
			m_mutexes[i] = createMutex();
		
		next
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Startup the network threads.
	'// -----------------------------------------------------------------------------------------------------------------
	method startup:byte(maxConnections:short, threadSleepTimer:int, listeningPort:short, backlog:byte = 0)
	
		if (isActive()) ..
			return false;
		
		DEBUG("Starting up network threads...");
		
		if (backlog > 100) ..
			backlog = 100;
		
		rabAssert(maxConnections > 0);
		
		if (maxConnections <= 0) ..
			return false;
		
		'// Create the socket for this peer.
		m_thisPeer = new RabNetSocket;
		m_thisPeer.m_clientSocket = createTCPSocket();
		m_thisPeer.m_listeningSocket = createTCPSocket();
		
		'// Find an available port and bind the listening socket to it.
		local port:int;
		if (listeningPort = 0) then
		
			port = RabNet.m_highestKnownAvailablePort;
			repeat
			
				port :+ 1;
			
			until (m_thisPeer.m_listeningSocket.bind(port) or port > 65535)
			
			if (port > 65535) then
			
				DEBUG("No port available. Closing socket...");
				m_thisPeer.destroy();
				shutdown(0);
				return false;
			
			end if
			
			DEBUG("Bound socket to next available port: " + port);
			RabNet.m_highestKnownAvailablePort = port;
		
		else
		
			if (m_thisPeer.m_listeningSocket.bind(listeningPort)) then
			
				DEBUG("Bound socket to specified port: " + listeningPort);
				port = listeningPort;
			
			else
			
				DEBUG("Could not bind to any port. Closing socket...");
				m_thisPeer.destroy();
				shutdown(0);
				return false;
			
			end if
		
		end if
		
		'// Start listening for connections. The backlog is the threshhold for systems that are currently
		'// trying to connect. Don't allow more simultanious connection tries than what this value is set to.
		DEBUG("Listening for connections (max: " + backlog + " connecting at the same time).");
		m_thisPeer.m_listeningSocket.listen(backlog);
		
		if (m_maxPeers = 0) then
		
			'// Don't allow more incoming connections than we have peers.
			if (m_maxIncomingConnections > maxConnections) ..
				m_maxIncomingConnections = maxConnections;
			
			m_maxPeers = maxConnections;
			
			rem
			local rsys:RemoteSystem;
			
			for local i:int = 0 until m_maxPeers
			
				rsys = new remoteSystem;
				rsys.m_isActive = false;
				rsys.m_systemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS;
				rsys.m_guid = RabNet.UNASSIGNED_RABNET_GUID;
				rsys.m_connectionMode = remoteSystem.NO_ACTION;
				m_remoteSystemList.addLast(rsys);
			
			next
			end rem
		
		end if
		
		if (m_endThreads) then
		
			m_endThreads = false;
			m_threadSleepTimer = threadSleepTimer;
			
			m_mySystemAddress = new SystemAddress;
			m_mySystemAddress.m_remoteAddress = RabNet.getRemoteIPFromHostName(RabNet.getLocalSystemHostName());
			m_mySystemAddress.m_port = port;
			
			DEBUG("Created socket with address: " + m_mySystemAddress.m_remoteAddress + ":" + ..
				m_mySystemAddress.m_port + ".");
			
			'// Launch main loop thread.
			local threadName:string;
			
			if (not m_isMainLoopThreadActive) then
			
				delay(1); '// This makes sure that the thread gets a unique name.
				threadName = "peer" + getTics();
				
				if (not RabMT.launchThread(threadName, updateNetworkLoop, self)) then
				
					shutdown(0);
					return false;
				
				end if
				
				m_thread = RabMT.getThread(threadName);
			
			end if
			
			'// Wait for the thread to activate. When it is activated it will set this flag to true.
			while (not m_isMainLoopThreadActive);
			
				delay(10);
			
			end while
		
		end if
		
		return true;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Shutdown the network threads.
	'// -----------------------------------------------------------------------------------------------------------------
	method shutdown(blockDuration:int)
	
		DEBUG("Shutting down network threads...");
		
		local anyActive:byte = false;
		local startWaitingTime:int;
		local time:int;
		
		'// Block execution for 'blockDuration' ms, to let the threads send as many leftover packets
		'// as possible in that time. If there are no connections, or if no connections are active,
		'// skip this step.
		if (blockDuration > 0) then
		
			for local rsys:remoteSystem = eachin m_remoteSystemList
			
				if (rsys.m_isActive) ..
					notifyAndFlagForShutdown(rsys.m_systemAddress);
			
			next
			
			time = getTics();
			startWaitingTime = time;
			while (time - startWaitingTime < blockDuration)
			
				anyActive = false;
				for local rsys:remoteSystem = eachin m_remoteSystemList
				
					if (rsys.m_isActive) then
					
						anyActive = true;
						exit;
					
					end if
				
				next
				
				'// If this system is out of packets to send, then stop waiting.
				if (anyActive = false) ..
					exit;
				
				'// This will probably cause the update thread to run which will probably
				'// send the disconnection notification.
				delay(15);
				time = getTics();
			
			end while
		
		end if
		
		local count:int = 0;
		while (m_isMainLoopThreadActive)
		
			count :+ 1;
			m_endThreads = true;
			delay(15);
			if (count >= 20) then
			
				detachThread(m_thread);
				exit;
			
			end if
		
		end while
		
		for local rsys:remoteSystem = eachin m_remoteSystemList
		
			rsys.m_isActive = false;
		
		next
		
		'// Setting 'm_maxPeers' to 0 allows 'm_remoteSystemList' to be reallocated in 'startup()'.
		m_maxPeers = 0;
		
		'// Free any packets the user didn't deallocate.
		local _packet:Packet;
		for _packet = eachin m_incomingPacketPool;
		
			_packet = null;
		
		next
		m_incomingPacketPool.clear();
		
		'// Close the sockets.
		if (m_thisPeer) ..
			m_thisPeer.destroy();
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Call this to connect to the specific host (ip or domain name) and server port. Calling this method and not
	'// calling 'setMaximumIncomingConnections()' acts as a dedicated client. Calling both acts as a true peer. You know
	'// the connection is successful when 'isConnected()' returns true or receive gets a packet with the type
	'// 'ID_CONNECTION_ACCEPTED'. If the connection is not successful, such as rejected connection, or no response, then
	'// neither of these things will happen. This method requires that you first call 'startup()'.
	'// -----------------------------------------------------------------------------------------------------------------
	method connect:byte(host:string, remotePort:int, passwordData:string, connectionAttemptCount:int = 7, ..
	                    timeBetweenAttempts:int = 500)
	
		local _connectionRequest:connectionRequest;
		
		m_mutexes[requestConnectionList_Mutex].lock();
		for local i:int = 0 until connectionAttemptCount
		
			_connectionRequest = new connectionRequest;
			
			_connectionRequest.m_ip = hostip(hostname(hostip(host)));
			if (not _connectionRequest.m_ip) then
			
				m_endThreads = true;
				m_mutexes[requestConnectionList_Mutex].unlock();
				return false;
			
			end if
			
			_connectionRequest.m_port = remotePort;
			_connectionRequest.m_password = passwordData;
			_connectionRequest.m_timer = timeBetweenAttempts;
			
			m_requestConnectionQueue.addLast(_connectionRequest);
		
		next
		m_mutexes[requestConnectionList_Mutex].unlock();
		
		return true;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Close the connection to another host (if we initiated the connection it will disconnect, if they did it will kick
	'// them out).
	'// -----------------------------------------------------------------------------------------------------------------
	method closeConnection(_systemAddress:SystemAddress, sendDisconnectionNotification:byte)
	
		local rsys:remoteSystem = getRemoteSystemFromSystemAddress(_systemAddress);
		
		if (sendDisconnectionNotification) ..
			rsys.m_connectionMode = remoteSystem.DISCONNECT_ASAP; ..
		else ..
			rsys.m_connectionMode = remoteSystem.DISCONNECT_ASAP_SILENTLY;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Cancel a pending connection attempt.
	'// -----------------------------------------------------------------------------------------------------------------
	method cancelConnectionAttempt(target:SystemAddress)
	
		local updatedRequestConnectionQueue:TList = new TList;
		
		for local cr:connectionRequest = eachin m_requestConnectionQueue
		
			if (cr.m_ip <> target.m_remoteAddress or cr.m_port <> target.m_port) ..
				updatedRequestConnectionQueue.addLast(cr);
		
		next
		
		m_requestConnectionQueue.clear();
		m_requestConnectionQueue = updatedRequestConnectionQueue;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Returns true if the specified system address is connected to this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	method isConnected:byte(_systemAddress:SystemAddress)
	
		local rsys:remoteSystem = getRemoteSystemFromSystemAddress(_systemAddress);
		return (rsys and rsys.m_isActive and rsys.m_connectionMode = remoteSystem.CONNECTED);
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Sets the maximum allowed number of incoming connections.
	'// -----------------------------------------------------------------------------------------------------------------
	method setMaximumIncomingConnections(numberAllowed:int)
	
		m_maxIncomingConnections = numberAllowed;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Returns the maximum allowed number of incoming connections.
	'// -----------------------------------------------------------------------------------------------------------------
	method getMaximumIncomingConnections:int()
	
		return m_maxIncomingConnections;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Sets the maximum allowed number of connected peers.
	'// -----------------------------------------------------------------------------------------------------------------
	method setMaximumNumberOfPeers(numberAllowed:int)
	
		m_maxPeers = numberAllowed;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Returns the maximum allowed number of connected peers.
	'// -----------------------------------------------------------------------------------------------------------------
	method getMaximumNumberOfPeers:int()
	
		return m_maxPeers;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Converts a system address to a remote system.
	'// -----------------------------------------------------------------------------------------------------------------
	method getRemoteSystemFromSystemAddress:remoteSystem(_systemAddress:SystemAddress)
	
		if (_systemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS) ..
			return null;
		
		for local rsys:remoteSystem = eachin m_remoteSystemList
		
			if (compareSystemAddresses(rsys.m_systemAddress, _systemAddress)) then
			
				if (rsys.m_isActive) ..
					return rsys;
			
			end if
		
		next
		
		for local rsys:remoteSystem = eachin m_remoteSystemList
		
			if (compareSystemAddresses(rsys.m_systemAddress, _systemAddress)) ..
				return rsys;
		
		next
		
		return null;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Compares to system addresses to see if they are equal.
	'// -----------------------------------------------------------------------------------------------------------------
	method compareSystemAddresses:byte(systemAddressA:SystemAddress, systemAddressB:SystemAddress)
	
		if (systemAddressA.m_remoteAddress = systemAddressB.m_remoteAddress and ..
		    systemAddressA.m_port = systemAddressB.m_port) ..
			return true;
		
		return false;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Converts a system address to a guid.
	'// -----------------------------------------------------------------------------------------------------------------
	method getGuidFromSystemAddress:long(_systemAddress:SystemAddress)
	
		local rsys:remoteSystem = getRemoteSystemFromSystemAddress(_systemAddress);
		if (rsys) ..
			return rsys.m_guid;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Return the system address of this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	method getThisSystemAddress:SystemAddress()
	
		return m_mySystemAddress;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Returns true if the network threads are running.
	'// -----------------------------------------------------------------------------------------------------------------
	method isActive:byte()
	
		return (m_endThreads = false);
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Send a packet to the specified system address. If broadcasting, the packet is sent to all connected remote
	'// systems, except for the specified system address.
	'// -----------------------------------------------------------------------------------------------------------------
	method send:byte(data:string, _systemAddress:SystemAddress, _broadcast:byte = false)
	
		if (not data) ..
			return false;
		
		if (_systemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS) ..
			return false;
		
		local _packet:Packet = new Packet;
		
		_packet.m_data = data.toCString();
		_packet.m_sender = m_mySystemAddress;
		_packet.m_recipient = _systemAddress;
		_packet.m_guid = m_guid;
		_packet.m_broadcast = _broadcast;
		
		pushPacket(_packet, false);
		
		return true;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Simulate that a remote system sent this peer a packet by creating a new packet and put it in the incoming packet
	'// pool, maskerading as if it was sent from the specified remote system.
	'// -----------------------------------------------------------------------------------------------------------------
	method sendLocal:byte(data:string, _remoteSystem:remoteSystem)
	
		if (not data) ..
			return false;
		
		if (not _remoteSystem) ..
			return false;
		
		if (_remoteSystem.m_systemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS) ..
			return false;
		
		if (_remoteSystem.m_guid = RabNet.UNASSIGNED_RABNET_GUID) ..
			return false;
		
		local _packet:Packet = new Packet;
		
		_packet.m_data = data.toCString();
		_packet.m_sender = _remoteSystem.m_systemAddress;
		_packet.m_recipient = m_mySystemAddress;
		_packet.m_guid = _remoteSystem.m_guid;
		_packet.m_broadcast = false;
		
		pushPacket(_packet, true);
		
		return true;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Same as 'sendLocal()', only we always send it to ourself, and thus no remote system is needed.
	'// -----------------------------------------------------------------------------------------------------------------
	method sendInternal:byte(data:string)
	
		if (not data) ..
			return false;
		
		local _packet:Packet = new Packet;
		
		_packet.m_data = data.toCString();
		_packet.m_sender = m_mySystemAddress;
		_packet.m_recipient = m_mySystemAddress;
		_packet.m_guid = m_guid;
		_packet.m_broadcast = false;
		
		pushPacket(_packet, true);
		
		return true;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Gets a packet from the incoming packet queue. Returns null if the packet is invalid or no packets are received.
	'// -----------------------------------------------------------------------------------------------------------------
	method receive:Packet()
	
		local _packet:Packet;
		
		if (m_incomingPacketPool.isEmpty()) ..
			return null;
		
		_packet = popPacket(true);
		
		if (not validPacket(_packet)) ..
			return null;
		
		return _packet;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Push a packet onto the packet pool.
	'// -----------------------------------------------------------------------------------------------------------------
	method pushPacket(_packet:Packet, incoming:byte)
	
		if (incoming) then
		
			DEBUG("Pushed a packet to the incoming packet pool.");
			
			m_mutexes[incomingPacketPool_Mutex].lock();
			m_incomingPacketPool.addLast(_packet);
			m_mutexes[incomingPacketPool_Mutex].unlock();
		
		else
		
			DEBUG("Pushed a packet to the outgoing packet pool.");
			
			m_mutexes[outgoingPacketPool_Mutex].lock();
			m_outgoingPacketPool.addLast(_packet);
			m_mutexes[outgoingPacketPool_Mutex].unlock();
		
		end if
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Pop a packet from the packet pool.
	'// -----------------------------------------------------------------------------------------------------------------
	method popPacket:Packet(incoming:byte)
	
		local _packet:Packet;
		
		if (incoming) then
		
			DEBUG("Poped a packet from the incoming packet pool.");
			
			m_mutexes[incomingPacketPool_Mutex].lock();
			_packet = Packet(m_incomingPacketPool.first());
			m_incomingPacketPool.removeFirst();
			m_mutexes[incomingPacketPool_Mutex].unlock();
		
		else
		
			DEBUG("Poped a packet from the outgoing packet pool.");
			
			m_mutexes[outgoingPacketPool_Mutex].lock();
			_packet = Packet(m_outgoingPacketPool.first());
			m_outgoingPacketPool.removeFirst();
			m_mutexes[outgoingPacketPool_Mutex].unlock();
		
		end if
		
		return _packet;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Returns true if the specified packet is valid.
	'// -----------------------------------------------------------------------------------------------------------------
	method validPacket:byte(_packet:Packet)
	
		if (_packet.m_sender and _packet.m_recipient and ..
			 _packet.m_guid and _packet.m_data and ..
			 _packet.m_sender <> RabNet.UNASSIGNED_SYSTEM_ADDRESS and ..
			 _packet.m_recipient <> RabNet.UNASSIGNED_SYSTEM_ADDRESS) ..
			return true;
		
		return false;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Deallocate a packet.
	'// -----------------------------------------------------------------------------------------------------------------
	method deallocatePacket(_packet:Packet)
	
		memFree(_packet.m_data);
		_packet = null;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Send a disconnect packet to a system and disconnect them when it is delivered.
	'// -----------------------------------------------------------------------------------------------------------------
	method notifyAndFlagForShutdown(_systemAddress:SystemAddress)
	
		local rsys:remoteSystem = getRemoteSystemFromSystemAddress(_systemAddress);
		rsys.m_connectionMode = remoteSystem.DISCONNECT_ASAP;
		send(chr(RabNet.ID_DISCONNECTION_NOTIFICATION), _systemAddress, false);
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Ping the specified target remote system.
	'// -----------------------------------------------------------------------------------------------------------------
	method pingInternal(target:SystemAddress)
	
		if (isActive() = false) ..
			return;
		
		local rsys:remoteSystem = getRemoteSystemFromSystemAddress(target);
		
		send(chr(RabNet.ID_INTERNAL_PING) + ..
			string(convertTimeToMS(currentTime()) + (m_curMS - rsys.m_clockDifferential)), ..
			target, false);
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Converts a time (ex. 14:31:57) to the corresponding milliseconds.
	'// -----------------------------------------------------------------------------------------------------------------
	method convertTimeToMS:int(time:string)
	
		local hourMS:int = int(strutil.word(0, time, asc(":"))) * 3600000;
		local minMS:int = int(strutil.word(1, time, asc(":"))) * 60000;
		local secMS:int = int(strutil.word(2, time, asc(":"))) * 1000;
		
		return hourMS + minMS + secMS;
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// The heart of RabNet. This is where everything happens. It updates the engine and handles connections and packets.
	'// -----------------------------------------------------------------------------------------------------------------
	method runUpdateCycle()
	
		if (not m_thisPeer.m_clientSocket or not m_thisPeer.m_listeningSocket) then
		
			m_endThreads = true;
			return;
		
		end if
		
		'// Update the internal millisecond clock.
		m_curMS = m_curMS = 1000 - (convertTimeToMS(currentTime()) - milliSecs()) mod 1000;
		
		local ip:int;
		local port:int;
		
		'// -------------------------------------------------------------------------------------------
		'//  * Processes all outgoing connection requests.
		'// -------------------------------------------------------------------------------------------
		if (not m_requestConnectionQueue.isEmpty()) then
		
			DEBUG("Processing all outgoing connection requests...");
			
			local success:byte = false;
			local timer:int;
			local connection:connectionRequest;
			
			m_mutexes[requestConnectionList_Mutex].lock();
			for connection = eachin m_requestConnectionQueue
			
				DEBUG("Trying to connect to " + connection.m_ip + ":" + connection.m_port + "...");
				if (m_thisPeer.m_clientSocket.connect(connection.m_ip, connection.m_port)) then
				
					success = true;
					exit;
				
				end if
				
				'// Remove the 1000 ms it takes for 'TSocket.connect()' to try.
				timer = connection.m_timer - 1000;
				
				if (timer > 0) ..
					delay(timer);
			
			next
			
			m_requestConnectionQueue.clear();
			m_mutexes[requestConnectionList_Mutex].unlock();
			
			'// If a connection was successful, send a connection request packet,
			'// with the guid of this system in the data.
			if (success) then
			
				DEBUG("Connection established.");
				
				local sysAddr:SystemAddress = new SystemAddress;
				sysAddr.m_remoteAddress = connection.m_ip;
				sysAddr.m_port = connection.m_port;
				
				send(chr(RabNet.ID_NEW_INCOMING_CONNECTION) + string(m_guid), sysAddr);
				
				local rsys:remoteSystem = new remoteSystem;
				rsys.m_systemAddress = sysAddr;
				rsys.m_weInitiatedTheConnection = true;
				rsys.m_connectionMode = remoteSystem.REQUESTED_CONNECTION;
				
				m_remoteSystemList.addLast(rsys);
				
				rsys.m_listLink = m_remoteSystemList.lastLink();
			
			'// If it wasn't successful, send an internal ID_CONNECTION_ATTEMPT_FAILED packet.
			else
			
				DEBUG("Connection failed.");
				sendInternal(chr(RabNet.ID_CONNECTION_ATTEMPT_FAILED));
			
			end if
		
		end if
		
		'// -------------------------------------------------------------------------------------------
		'//  * Accept an incoming connection. If accepting a connection, add it to
		'//    'm_remoteSystemList', but don't make it active. It gets activated when we receive a
		'//    valid connection packet.
		'// -------------------------------------------------------------------------------------------
		local socket:TSocket = m_thisPeer.m_listeningSocket.accept(10);
		if (socket) then
		
			DEBUG("Accepting an incoming connection...");
			
			local sysAddr:SystemAddress = new SystemAddress;
			sysAddr.m_remoteAddress = socket.remoteIP();
			sysAddr.m_localAddress = socket.localIP();
			sysAddr.m_port = socket.remotePort();
			
			DEBUG("IP Address: " + sysAddr.m_remoteAddress);
			DEBUG("Port: " + sysAddr.m_port);
			
			local rsys:remoteSystem = new remoteSystem;
			rsys.m_systemAddress = sysAddr;
			rsys.m_weInitiatedTheConnection = false;
			rsys.m_connectionMode = remoteSystem.HANDLING_CONNECTION_REQUEST;
			
			m_remoteSystemList.addLast(rsys);
			
			rsys.m_listLink = m_remoteSystemList.lastLink();
		
		end if
		
		'// -------------------------------------------------------------------------------------------
		'//  * Send an outgoing packet.
		'// -------------------------------------------------------------------------------------------
		if (not m_outgoingPacketPool.isEmpty()) then
		
			local _packet:Packet = popPacket(false);
			
			DEBUG("Trying to send an outgoing packet...");
			DEBUG("Packet sender: " + _packet.m_sender.m_remoteAddress);
			DEBUG("Packet recipient: " + _packet.m_recipient.m_remoteAddress + ":" + _packet.m_recipient.m_port);
			DEBUG("Packet data: " + string.fromCString(_packet.getData()));
			
			'// Send if this is a valid packet.
			if (validPacket(_packet)) then
			
				DEBUG("Packet is valid. Sending...");
				
				'// Send to all connected remote systems, except one.
				if (_packet.m_broadcast) then
				
					for local rsys:remoteSystem = eachin m_remoteSystemList
					
						if (rsys.m_systemAddress.m_remoteAddress <> m_mySystemAddress.m_remoteAddress and ..
						    rsys.m_systemAddress.m_port <> m_mySystemAddress.m_port and ..
						    rsys.m_systemAddress.m_remoteAddress <> _packet.m_recipient.m_remoteAddress and ..
						    rsys.m_systemAddress.m_port <> _packet.m_recipient.m_port) then
						
							sendto_(m_thisPeer.m_clientSocket._socket, _packet.m_data, _packet.m_length, 0, ..
								_packet.m_recipient.m_remoteAddress, _packet.m_sender.m_port);
						
						end if
					
					next
				
				'// Send to a specific remote system.
				else
				
					local sysAddr:SystemAddress = new SystemAddress;
					sysAddr.m_remoteAddress = _packet.m_recipient.m_remoteAddress;
					sysAddr.m_port = _packet.m_recipient.m_port;
					
					'// Make sure the remote system is connected, then send.
					if (getRemoteSystemFromSystemAddress(sysAddr)) ..
						sendto_(m_thisPeer.m_clientSocket._socket, _packet.m_data, _packet.m_length, 0, ..
							sysAddr.m_remoteAddress, sysAddr.m_port);
				
				end if
				
				'// Deallocate the packet.
				DEBUG("Deallocating packet...");
				deallocatePacket(_packet);
			
			end if
		
		end if
		
		'// -------------------------------------------------------------------------------------------
		'//  * Collect an incoming packet.
		'// -------------------------------------------------------------------------------------------
		local _packet:Packet = new Packet;
		_packet.m_length = m_thisPeer.m_clientSocket.readAvail();
		if (_packet.m_length) then
		
			DEBUG("Collected an incoming packet...");
			
			_packet = new Packet;
			
			ip = 0;
			port = 0;
			
			recvfrom_(m_thisPeer.m_clientSocket._socket, _packet.m_data, _packet.m_length, 0, ip, port);
			
			'// Make sure that data was collected/found, and that we could determine who sent the
			'// packet.
			if (_packet.m_data or not ip or not port) then
			
				local sysAddr:SystemAddress = new SystemAddress;
				sysAddr.m_remoteAddress = ip;
				sysAddr.m_port = port;
				
				DEBUG("Packet sender: " + ip + ":" + port);
				
				ip = 0;
				port = 0;
				
				_packet.m_sender = sysAddr;
				
				local data:string = string.fromCString(_packet.getData());
				
				DEBUG("Packet data: " + data);
				
				'// Process some packets internally.
				'// Probable feature: If we're going to add passwords to accept packets, we handle that
				'//   here.
				select (data[0])
				
					case RabNet.ID_NEW_INCOMING_CONNECTION
						DEBUG("Packet id: " + RabNet.ID_NEW_INCOMING_CONNECTION);
						
						local rsys:remoteSystem = getRemoteSystemFromSystemAddress(sysAddr);
						local guid:long = long(data[1..]);
						
						if (rsys and guid) ..
							rsys.m_guid = guid;
						
						_packet.m_guid = guid;
						
						if (m_remoteSystemList.count() < m_maxPeers) then
						
							rsys.m_isActive = true;
							rsys.m_connectionMode = remoteSystem.CONNECTED;
							
							send(chr(RabNet.ID_CONNECTION_REQUEST_ACCEPTED) + string(m_guid), sysAddr);
						
						else
						
							send(chr(RabNet.ID_NO_FREE_INCOMING_CONNECTIONS), sysAddr);
							rsys.m_listLink.remove();
						
						end if
					case RabNet.ID_CONNECTION_REQUEST_ACCEPTED
						DEBUG("Packet id: " + RabNet.ID_CONNECTION_REQUEST_ACCEPTED);
						
						local rsys:remoteSystem = getRemoteSystemFromSystemAddress(sysAddr);
						local guid:long = long(data[1..]);
						
						if (rsys and guid) ..
							rsys.m_guid = guid;
						
						_packet.m_guid = guid;
						
						rsys.m_isActive = true;
						rsys.m_connectionMode = remoteSystem.CONNECTED;
					case RabNet.ID_DISCONNECTION_NOTIFICATION
						DEBUG("Packet id: " + RabNet.ID_DISCONNECTION_NOTIFICATION);
						local rsys:remoteSystem = getRemoteSystemFromSystemAddress(sysAddr);
						rsys.m_isActive = false;
						rsys.m_connectionMode = remoteSystem.DISCONNECT_ON_NO_ACK;
					case RabNet.ID_INTERNAL_PING
						DEBUG("Packet id: " + RabNet.ID_INTERNAL_PING);
						local rsys:remoteSystem = getRemoteSystemFromSystemAddress(sysAddr);
						local guid:long = long(data[1..]);
						send(chr(RabNet.ID_CONNECTED_PONG) + string(guid) + ..
							string(convertTimeToMS(currentTime()) + m_curMS - rsys.m_clockDifferential), ..
							sysAddr, false);
					case RabNet.ID_CONNECTED_PONG
						DEBUG("Packet id: " + RabNet.ID_CONNECTED_PONG);
						
						local rsys:remoteSystem = getRemoteSystemFromSystemAddress(sysAddr);
						
						local sendPingTime:int = data[1..8].toInt();
						local sendPongTime:int = data[9..16].toInt();
						
						local time:int = convertTimeToMS(currentTime()) + m_curMS;
						
						local ping:int;
						local lastPing:int;
						
						if (time > sendPingTime) ..
							ping = time - sendPingTime; ..
						else ..
							ping = 0;
						
						lastPing = rsys.m_lastPing;
						
						'// Ignore super high spikes in the average.
						if (lastPing <= 0 or ((ping < (lastPing * 3)) and ping < 1200)) then
						
							rsys.m_lastPing = ping;
							rsys.m_clockDifferential = sendPongTime - (time/2 + sendPingTime/2);
							
							if (rsys.m_lowestPing > ping) ..
								rsys.m_lowestPing = ping;
						
						end if
					default
						DEBUG("Packet id: Unknown. Assuming it's a user defined identifier.");
				
				end select
				
				if (not _packet.m_guid) then
				
					if (getRemoteSystemFromSystemAddress(sysAddr)) ..
						_packet.m_guid = getGuidFromSystemAddress(sysAddr); ..
					else ..
						_packet.m_guid = RabNet.UNASSIGNED_RABNET_GUID;
				
				end if
				
				DEBUG("Pushing packet to incoming packet pool...");
				pushPacket(_packet, true);
			
			end if
		
		end if
		
		'// -------------------------------------------------------------------------------------------
		'//  * Manage the connections to all remote systems.
		'// -------------------------------------------------------------------------------------------
		for local rsys:remoteSystem = eachin m_remoteSystemList
		
			local time:int = getTics();
			
			'// Check if the connection to the remote system has been lost.
			if ((rsys.m_connectionMode = remoteSystem.CONNECTED or ..
			    rsys.m_connectionMode = remoteSystem.REQUESTED_CONNECTION or ..
			    rsys.m_connectionMode = remoteSystem.HANDLING_CONNECTION_REQUEST or ..
			    rsys.m_connectionMode = remoteSystem.UNVERIFIED_SENDER) and ..
			    time > rsys.m_connectionTime and time - rsys.m_connectionTime > 10000) then
			
				'// Send an ID_CONNECTION_LOST packet and close the connection.
				sendLocal(string(RabNet.ID_CONNECTION_LOST), rsys);
				closeConnection(rsys.m_systemAddress, false);
			
			'// Check if the remote system has disconnected.
			else if (rsys.m_connectionMode = remoteSystem.DISCONNECT_ASAP or ..
			         rsys.m_connectionMode = remoteSystem.DISCONNECT_ASAP_SILENTLY or ..
			         rsys.m_connectionMode = remoteSystem.DISCONNECT_ON_NO_ACK) then
			
				'// Remove the remote system.
				rsys.m_listLink.remove();
			
			end if
			
			'// Ping this remote system if it is time to do so.
			if (rsys.m_connectionMode = remoteSystem.CONNECTED and time > rsys.m_nextPingTime) then
			
				rsys.m_nextPingTime = time + 5000;
				pingInternal(rsys.m_systemAddress);
			
			end if
		
		next
	
	end method
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// This is the main thread.
	'// -----------------------------------------------------------------------------------------------------------------
	function updateNetworkLoop:object(peer:object)
	
		local _peer:RabPeer = RabPeer(peer);
		
		_peer.m_isMainLoopThreadActive = true;
		
		while (not _peer.m_endThreads)
		
			_peer.runUpdateCycle();
			delay(_peer.m_threadSleepTimer);
		
		end while
		
		_peer.m_isMainLoopThreadActive = false;
		
		return null;
	
	end function
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Set this to true to terminate the thread execution.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_endThreads:byte = true;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// A table of all mutexes for this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_mutexes:TMutex[NUMBER_OF_RABPEER_MUTEXES];
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// List of all remote systems this peer is connected to.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_remoteSystemList:TList = new TList;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// The rabnet socket of this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_thisPeer:RabNetSocket;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// The system address of this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_mySystemAddress:SystemAddress = RabNet.UNASSIGNED_SYSTEM_ADDRESS;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Unique identifier for this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_guid:int = RabNet.UNASSIGNED_RABNET_GUID;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// How long the main thread should wait (ms) between update cycles.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_threadSleepTimer:short;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// True if the main loop thread for this peer is active.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_isMainLoopThreadActive:byte = false;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Maximum number of peers allowed.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_maxPeers:int = 0;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Maximum number of connections allowed.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_maxIncomingConnections:int = 0;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Packets that have been received.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_incomingPacketPool:TList = new TList;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Packets that are about to be sent.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_outgoingPacketPool:TList = new TList;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Que of connection requests to a host system.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_requestConnectionQueue:TList = new TList;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// Current millisecond of this second. Used to synchronize clocks when pinging.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_curMS:int;
	
	'// -----------------------------------------------------------------------------------------------------------------
	'// A pointer to the thread for this peer.
	'// -----------------------------------------------------------------------------------------------------------------
	field m_thread:TThread;

end type
